UnityEditorのUndoで気をつけることファイナル


概要

ほぼすべての人類に無関係。十分にロックイン事案。


Undo自体の実装の話はこっち。

「UnityEditorのUndo/Redoシステムについて【解決編】【最新】のコピーそして完結」

http://sassembla.github.io/Public/2015:09:17%203-14-23/2015:09:17%203-14-23.html

今回は、なるべく面倒臭く無い方法で、管理される側のオブジェクトに辞書を使ったりする話。

端的にいうと最後、値をどう持つかで、値の保持にDictionaryを使うと辛い部分があった。

これはScriptableObjectを全開で使ってても、実際のデータの型に関連するので、例外なくつらい。


Dictionaryを使うと何が辛いのか

辞書 Dictionary をデータ構造に使うと、その部分がUndo効かない。

抽象的なコード書くと、


Undo.RecordObject(this, “Update Dictionary Value”);

myDict[“key”] = “val”;


みたいなことをしても、myDictの値がUndoできない。というかUndo履歴に乗らない。

[SerializeField] 振ってても、これは別の理由で効かないっぽい。

同様にList<string> とかに対して、


Undo.RecordObject(this, “Update List Value”);

myList.Add(“val”);


とかやると、これは効く。



違いを追っていく過程でわかったこと

いろいろ試していたら、次のようなことがわかった。完全にやっくしぇーびんぐ感があった。


・ISerializationCallbackReceiver とかはUndoとは関係無いっぽい

(Undoにはserialize的な特性は必要でも実務として特定のserialize処理が走ることは無いっぽくてISerializationCallbackReceiver実装しても呼ばれない


・Dict -> List + List に分解するとうまくいく

Dictionary<string, string> -> List<string> keys + List<string> values に分解したら、Undoは働いた


・調子に乗ってクラス化 + 型パラメータ化 -> 死ぬ 

型パラメータが入るとだめくさい。

例えば下記のような構成は死ぬ。


[Serializable] public class SerializablePseudoDictionary<TKey, TValue> {

[SerializeField] private List<TKey> keys = new List<TKey>();

[SerializeField] private List<TValue> values = new List<TValue>();

. . .


こういうの定義できると楽だったんだが。



結論

List + GenericならUndo対象にできるので、Dictionary + Generics -> List<string> keys & List<string> values と分解できるようなクラス作ってやると瞬殺できる。


実際AssetGraphで使うことになった。 辞書っぽいもの作ってもUndoに対応できる。

https://github.com/unity3d-jp/AssetGraph/blob/0.7.6/Assets/AssetGraph/Editor/Libs/SerializablePseudoDictionary.cs

抜粋。

using UnityEngine;


using System;

using System.Linq;

using System.Collections.Generic;


namespace AssetGraph {

/*

string key & string value only.

because generic dictionary class cannot undo.


write -> Add(k,v) -> new dict -> keys, values

read <- ReadonlyDict() <- new dict <- keys, values

*/

[Serializable] public class SerializablePseudoDictionary {

[SerializeField] private List<string> keys = new List<string>();

[SerializeField] private List<string> values = new List<string>();


public SerializablePseudoDictionary (Dictionary<string, string> baseDict) {

var dict = new Dictionary<string, string>(baseDict);


keys = dict.Keys.ToList();

values = dict.Values.ToList();

}


public void Add (string key, string val) {

var dict = new Dictionary<string, string>();

for (var i = 0; i < keys.Count; i++) {

var currentKey = keys[i];

var currentVal = values[i];

dict[currentKey] = currentVal;

}


// add or update parameter.

dict[key] = val;


keys = new List<string>(dict.Keys);

values = new List<string>(dict.Values);

}


public bool ContainsKey (string key) {

var dict = new Dictionary<string, string>();

for (var i = 0; i < keys.Count; i++) {

var currentKey = keys[i];

var currentVal = values[i];

dict[currentKey] = currentVal;

}


return dict.ContainsKey(key);

}


public Dictionary<string, string> ReadonlyDict () {

var dict = new Dictionary<string, string>();

if (keys == null) return dict;


for (var i = 0; i < keys.Count; i++) {

var key = keys[i];

var val = values[i];

dict[key] = val;

}


return dict;

}

}


お察しのとおり、read, writeに辞書化を挟んでるんで、使いやすいような気はあんましない。

Undo対象の数が少なかったのと、Undo動作自体を綺麗にまとめておくことができたので、実装と導入と改変は楽だった。


ただUnityとしか関係無い特殊な状況だ。


感想

試してないけどhashとかだとうまく動くんじゃ無いだろうか。


厄介なのは、Editor以外でもSerializable使っていろんなものをSerialize可能にすることができるんだけど、

これは結局ゲーム中でも使える手段だぜってところなんだけど、

・ListはOKでDictはダメ

・Serializableつけてもダメ

っていうのがある時点で、まあ、独自色強くなるんで、できうる限りゲームでDictのSerializeは使わないほうがいいだろうな、と思った。


ちなみにゲーム中( = 非Editor)の場合は、ISerializationCallbackReceiverまわりの関数が呼ばれるらしい。

Editorではそんなことなかったんで、まあ、はい。


無理にこういう[Serializable] + 独自Serializer実装するくらいなら、下記を実行するといいと思う。


a.Unityさんに殴り込んで「Serializableの有効範囲広げてや」って言う

b.[Serializable] を使ってDictionaryのSerializeが必要な状態を避ける、あるいはDictionary以外のものに対して[Serializable] を使う

c.[Serializable] を使わず自分で何かに変換する


b,cどちらの手段を選ぶにしても、Unityに「お前SerializeField効かない型があるの辛いぞ」っていう話をするのはアリだと思う。

自分からはします。